import chalk from 'chalk'
import { spawn } from 'child_process'
import fs from 'fs-extra'
import { readFile } from 'fs/promises'
import { once } from 'lodash-es'
import path from 'path'
import { PluginHooks } from 'rollup'
import * as pkg from '../../package.json'
import { COMMONJS_REQUIRE, CWD } from '../constants'
import { ResolvedOptions } from '../options'
import { getSourcePath, hashString, removeExtension } from '../utils'
import { getHelper as getBaseHelp } from './base'
import golangModule, { GO, TINY_GO } from './modules/golang'
import { generageImports } from './modules/_utils'
import { defineHelper } from './_utils'

const _getGoCompiler = async (resolvedGoOptions: ResolvedOptions['golang']) => {
  const dir = path.resolve(CWD, 'node_modules/.rollup-plugin-wasm/golang')
  await fs.mkdirs(dir)

  if (
    GO &&
    (await new Promise<boolean>((resolve, reject) => {
      const goVersionProccess = spawn(GO!, ['version'])
      let stdout = ''
      let stderr = ''

      goVersionProccess.on('error', () => {
        resolve(false)
      })

      goVersionProccess.on('spawn', () => {
        setTimeout(() => {
          goVersionProccess.kill('SIGKILL')
        }, 500)
      })

      goVersionProccess.stdout.on('data', (chunk) => {
        stdout += chunk.toString()
      })

      goVersionProccess.stderr.on('data', (chunk) => {
        stderr += chunk.toString()
      })

      goVersionProccess.on('exit', () => {
        const out = stdout + '\n' + stderr
        const re = /\bversion .*?(?<version>\d+\.\d+\.\d+)/s

        const match = out.match(re)

        if (match) {
          resolve(true)
          console.info(chalk.yellow`[${pkg.name}]: Found go. Version is ${match.groups?.version}.`)
        } else {
          resolve(false)
        }
      })
    }))
  ) {
    return async (input: string) => {
      return new Promise<{ binFileName: string }>((resolve, reject) => {
        const outputFile = path.join(
          dir,
          path.parse(input).name + '-' + hashString(input, 'hex', 8) + '.wasm'
        )

        const argv = [...resolvedGoOptions.goArgv, '-o', outputFile, input]
        const goProcess = spawn(GO!, argv, {
          cwd: CWD,
          env: {
            ...process.env,
            GOARCH: 'wasm',
            GOOS: 'js',
          },
        })

        let stderr = ''
        goProcess.stderr.on('data', (chunk) => {
          stderr += chunk.toString()
        })
        goProcess.on('exit', async (code, signal) => {
          if (signal || code) {
            console.error(chalk.red`✖ Golang-module "${input}" compiled failure: ${stderr}`)
            return reject(`Compiler exited abnormally. code: ${code}, signal: ${signal}.`)
          }
          console.info(chalk.yellow`✔ Golang-module "${input}" compiled successfully.`)
          resolve({ binFileName: outputFile })
        })
        goProcess.on('spawn', () =>
          console.info(chalk.blue`⭐ Start compiling golang-module "${input}".`)
        )
        goProcess.on('error', (err) => {
          console.error(chalk.red`✖ Golang-module "${input}" compiled failure: ${err?.toString()}`)
          reject(err)
        })
      })
    }
  }

  console.error(chalk.red`[${pkg.name}]: commond 'go' not found. Did you install it?`)
}

const _getTinyGoCompiler = async (resolvedGoOptions: ResolvedOptions['golang']) => {
  const dir = path.resolve(CWD, 'node_modules/.rollup-plugin-wasm/golang')
  await fs.mkdirs(dir)

  if (
    TINY_GO &&
    (await new Promise<boolean>((resolve, reject) => {
      const tinygoVersionProccess = spawn(TINY_GO!, ['version'])
      let stdout = ''
      let stderr = ''

      tinygoVersionProccess.on('error', () => {
        resolve(false)
      })

      tinygoVersionProccess.on('spawn', () => {
        setTimeout(() => {
          tinygoVersionProccess.kill('SIGKILL')
        }, 500)
      })

      tinygoVersionProccess.stdout.on('data', (chunk) => {
        stdout += chunk.toString()
      })

      tinygoVersionProccess.stderr.on('data', (chunk) => {
        stderr += chunk.toString()
      })

      tinygoVersionProccess.on('exit', () => {
        const out = stdout + '\n' + stderr
        const re = /\bversion .*?(?<version>\d+\.\d+\.\d+)/s

        const match = out.match(re)

        if (match) {
          resolve(true)
          console.info(chalk.yellow`[${pkg.name}]: Found go. Version is ${match.groups?.version}.`)
        } else {
          resolve(false)
        }
      })
    }))
  ) {
    return async (input: string) => {
      return new Promise<{ binFileName: string }>((resolve, reject) => {
        const outputFile = path.join(
          dir,
          removeExtension(input) + '-' + hashString(input, 'hex', 8) + '.wasm'
        )

        const argv = [...resolvedGoOptions.tinygoArgv, '-o', outputFile]
        const tinygoProcess = spawn(TINY_GO!, argv, {
          cwd: CWD,
        })

        let stderr = ''
        tinygoProcess.stderr.on('data', (chunk) => {
          stderr += chunk.toString()
        })
        tinygoProcess.on('exit', async (code, signal) => {
          if (signal || code) {
            console.error(chalk.red`✖ Golang-module "${input}" compiled failure: ${stderr}`)
            return reject(`Compiler exited abnormally. code: ${code}, signal: ${signal}.`)
          }
          console.info(chalk.yellow`✔ Golang-module "${input}" compiled successfully.`)
          resolve({ binFileName: outputFile })
        })
        tinygoProcess.on('spawn', () =>
          console.info(chalk.blue`⭐ Start compiling golang-module "${input}".`)
        )
        tinygoProcess.on('error', (err) => {
          console.error(chalk.red`✖ Golang-module "${input}" compiled failure: ${err?.toString()}`)
          reject(err)
        })
      })
    }
  }

  console.error(chalk.red`[${pkg.name}]: commond 'tinygo' not found. Did you install it?`)
}

export const getHelper = defineHelper((options, registerVirtualModule) => {
  const getGoCompiler = once(() => _getGoCompiler(options.golang))
  const getTinyGoCompiler = once(() => _getTinyGoCompiler(options.golang))
  const utilsHelper = getBaseHelp(options)

  const GOLANG_INTERNAL_PREFIX = 'WASM-GOLANG:'
  const GO_INTERNAL_SUFFIX = '?go'
  const GO_MODULE_RE = /^(?:go):(.+)$/
  const TINYGO_INTERNAL_SUFFIX = '?tinygo'
  const TINYGO_MODULE_RE = /^(?:tinygo):(.+)$/

  const resolveId: PluginHooks['resolveId'] = async function (id, importer, options) {
    let match

    if (id.endsWith(COMMONJS_REQUIRE)) {
      id = id.slice(0, id.length - COMMONJS_REQUIRE.length)
    }

    if ((match = id.match(GO_MODULE_RE))) {
      const result = await getSourcePath(this, match[1], importer, ['.go'], options)
      if (result) return GOLANG_INTERNAL_PREFIX + result + GO_INTERNAL_SUFFIX
    } else if ((match = id.match(TINYGO_MODULE_RE))) {
      const result = await getSourcePath(this, match[1], importer, ['.go'], options)
      if (result) return GOLANG_INTERNAL_PREFIX + result + TINYGO_INTERNAL_SUFFIX
    } else if (
      id.startsWith(GOLANG_INTERNAL_PREFIX) &&
      (id.endsWith(GO_INTERNAL_SUFFIX) || id.endsWith(TINYGO_INTERNAL_SUFFIX))
    ) {
      return id
    }
  }

  const load: PluginHooks['load'] = async function (id) {
    const rawId = id
    if (id.startsWith(GOLANG_INTERNAL_PREFIX)) {
      id = id.slice(GOLANG_INTERNAL_PREFIX.length)

      if (id.endsWith(GO_INTERNAL_SUFFIX)) {
        id = id.slice(0, id.length - GO_INTERNAL_SUFFIX.length)
        const compile = await getGoCompiler()
        if (!compile) return

        const { binFileName } = await compile(id)
        const inline = options.inline(binFileName, id)
        const source = await readFile(binFileName)

        utilsHelper.addWatchFile(this, rawId, id)

        if (inline) {
          return `
            ${generageImports(
              [golangModule, ['go_createInstantiateFromBuffer']],
              [utilsHelper.getInlineBufferModule(source), [], 'source']
            )}
            export const { instantiate, instantiateSync } = go_createInstantiateFromBuffer(source);
            const _getExports = () => { try { return instantiateSync().exports } catch(err) { console.error(err) } };
            export default /*#__PURE__*/ _getExports();
          `
        } else {
          return `
            ${generageImports([golangModule, ['go_createInstantiateFromPath']])}
            const _fileName = ${await utilsHelper.fileToStringifyUrl(this, binFileName, source)};
            export const { instantiate, instantiateSync } = go_createInstantiateFromPath(_fileName);
            const _getExports = () => { try { return instantiateSync().exports } catch(err) { console.error(err) } };
            export default /*#__PURE__*/ _getExports();
          `
        }
      } else if (id.endsWith(TINYGO_INTERNAL_SUFFIX)) {
        id = id.slice(0, id.length - TINYGO_INTERNAL_SUFFIX.length)
        const compile = await getTinyGoCompiler()
        if (!compile) return

        const { binFileName } = await compile(id)
        const inline = options.inline(binFileName, id)
        const source = await readFile(binFileName)

        if (inline) {
          return `
            ${generageImports(
              [golangModule, ['tinygo_createInstantiateFromBuffer']],
              [utilsHelper.getInlineBufferModule(source), [], 'source']
            )}
            export const { instantiate, instantiateSync } = tinygo_createInstantiateFromBuffer(source);
            const _getExports = () => { try { return instantiateSync().exports } catch(err) { console.error(err) } };
            export default /*#__PURE__*/ _getExports();
          `
        } else {
          const name = path.parse(id).name
          return `
            ${generageImports([golangModule, ['tinygo_createInstantiateFromPath']])}
            const _fileName = ${await utilsHelper.fileToStringifyUrl(this, binFileName, source)};
            export const { instantiate, instantiateSync } = tinygo_createInstantiateFromPath(_fileName);
            const _getExports = () => { try { return instantiateSync().exports } catch(err) { console.error(err) } };
            export default /*#__PURE__*/ _getExports();
          `
        }
      }
    } else if (options.golang.filter(id)) {
      const stringifyId = JSON.stringify(
        GOLANG_INTERNAL_PREFIX +
          id +
          (options.golang.tinygo ? TINYGO_INTERNAL_SUFFIX : GO_INTERNAL_SUFFIX)
      )
      return `export * from ${stringifyId};export {default} from ${stringifyId};`
    }
  }

  return { resolveId, load }
})
